Приоритизируем гипотезы для увеличения выручки интернет-магазина, запустим A/B-тест и проанализируем результаты.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
import math
from IPython.display import display
from scipy import stats as st
Загрузим датасет hypothesis и выведем на экран.
hypothesis = pd.read_csv('/datasets/hypothesis.csv')
hypothesis
Применим фреймворк ICE для приоритизации гипотез. Отсортируем их по убыванию приоритета и построим гистограмму.
hypothesis['ICE'] = hypothesis['Impact'] * hypothesis['Confidence'] / hypothesis['Efforts']
hypothesis_ICE = hypothesis.sort_values(by='ICE')[['Hypothesis', 'ICE']]
fig = px.bar(hypothesis_ICE, x='ICE', y='Hypothesis', orientation='h', labels={'ICE': 'ICE', 'Hypothesis':'Гипотеза'})
fig.update_layout(title_text='Приоритизация гипотез по фреймворку ICE', yaxis_showticklabels=False)
fig.show()
Применим фреймворк RICE для приоритизации гипотез. Отсортируем их по убыванию приоритета и построим гистограмму.
hypothesis['RICE'] = hypothesis['Reach'] * hypothesis['Impact'] * hypothesis['Confidence'] / hypothesis['Efforts']
hypothesis_RICE = hypothesis.sort_values(by='RICE')[['Hypothesis', 'RICE']]
fig = px.bar(hypothesis_RICE, x='RICE', y='Hypothesis', orientation='h', labels={'RICE': 'RICE', 'Hypothesis':'Гипотеза'})
fig.update_layout(title_text='Приоритизация гипотез по фреймворку RICE', yaxis_showticklabels=False)
fig.show()
При применении RICE вместо ICE с третьего места на первое вышла гипотеза "Добавить форму подписки на все основные страницы, чтобы собрать базу клиентов для email-рассылок", потому что она с достаточной силой повлияет на всех пользователей. Гипотеза "Запустить акцию, дающую скидку на товар в день рождения" опустилась на пятое место из-за низкого охвата: чтобы затронуть всех пользователей потребуется целый год.
Загрузим данные и изучим общую информацию.
orders = pd.read_csv('/datasets/orders.csv')
orders.info()
visitors = pd.read_csv('/datasets/visitors.csv')
visitors.info()
Преобразуем строки в столбце date таблицы orders в формат даты.
orders['date'] = pd.to_datetime(orders['date'], format='%Y-%m-%d')
Преобразуем строки в столбце date таблицы visitors в формат даты.
visitors['date'] = pd.to_datetime(visitors['date'], format='%Y-%m-%d')
visitors_intersection = np.intersect1d(
orders[orders['group'] == 'A']['visitorId'], \
orders[orders['group'] == 'B']['visitorId'])
visitors_intersection
len(visitors_intersection)
len(visitors_intersection) / orders.shape[0]
Таких пользователей 58, что составляет менее 5% выборки. Следует ли исключать их из теста? 🤔
Например, пользователь 8300375 отметился сразу в двух группах.
orders[orders['visitorId'] == 8300375]
А вот пересечений заказов нет.
len(np.intersect1d(orders[orders['group'] == 'A']['transactionId'], orders[orders['group'] == 'B']['transactionId']))
Попарно сравним параметры каждой выборки по группам.
ordersAggregated = orders.groupby(
['date','group']).agg({'transactionId':'nunique', 'visitorId':'nunique', 'revenue':'sum'}).reset_index()
ordersAggregated[['transactionId_cum', 'visitorId_cum', 'revenue_cum']] = pd.concat(
[ordersAggregated[ordersAggregated['group'] == 'A'][['transactionId', 'visitorId', 'revenue']].cumsum(), \
ordersAggregated[ordersAggregated['group'] == 'B'][['transactionId', 'visitorId', 'revenue']].cumsum()])
visitorsAggregated = visitors.groupby(['date','group']).agg({'visitors':'sum'}).reset_index()
visitorsAggregated['visitors_cum'] = pd.concat(
[visitorsAggregated[visitorsAggregated['group'] == 'A']['visitors'].cumsum(), \
visitorsAggregated[visitorsAggregated['group'] == 'B']['visitors'].cumsum()])
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Гистограмма выручки заказа по группам')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'A']['revenue'].index,
ordersAggregated[ordersAggregated['group'] == 'A']['revenue'], label='A')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'B']['revenue'].index,
ordersAggregated[ordersAggregated['group'] == 'B']['revenue'], label='B')
ax = plt.gca()
ax.set_ylabel('Выручка заказа')
ax.get_xaxis().set_visible(False)
plt.legend();
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Гистограмма количества заказов по группам')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'A']['transactionId'].index,
ordersAggregated[ordersAggregated['group'] == 'A']['transactionId'], label='A')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'B']['transactionId'].index,
ordersAggregated[ordersAggregated['group'] == 'B']['transactionId'], label='B')
ax = plt.gca()
ax.set_ylabel('Количество заказов')
ax.get_xaxis().set_visible(False)
plt.legend();
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Гистограмма количества покупателей по группам')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'A']['visitorId'].index,
ordersAggregated[ordersAggregated['group'] == 'A']['visitorId'], label='A')
plt.bar(ordersAggregated[ordersAggregated['group'] == 'B']['visitorId'].index,
ordersAggregated[ordersAggregated['group'] == 'B']['visitorId'], label='B')
ax = plt.gca()
ax.set_ylabel('Количество покупателей')
ax.get_xaxis().set_visible(False)
plt.legend();
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Гистограмма количества посетителей по группам')
plt.bar(visitorsAggregated[visitorsAggregated['group'] == 'A']['visitors'].index,
visitorsAggregated[visitorsAggregated['group'] == 'A']['visitors'], label='A')
plt.bar(visitorsAggregated[visitorsAggregated['group'] == 'B']['visitors'].index,
visitorsAggregated[visitorsAggregated['group'] == 'B']['visitors'], label='B')
ax = plt.gca()
ax.set_ylabel('Количество посетителей')
ax.get_xaxis().set_visible(False)
plt.legend();
Вывод: за исключением всплеска выручки группы B 19 августа группы соразмерны.
Соберём агрегированные кумулятивные по дням данные о заказах в несколько действий. Сгруппируем таблицу orders по дате и группе A/B-теста и посчитаем количество уникальных заказов, покупателей и суммарную выручку. Сбросим индексы методом reset_index().
ordersAggregated = orders.groupby(
['date','group']).agg({'transactionId':'nunique', 'visitorId':'nunique', 'revenue':'sum'}).reset_index()
ordersAggregated.head(10)
Сконкатенируем по группам кумулятивные данные о заказах, покупателях и выручке методом pd.concat() и сохраним в столбцы transactionId_cum, visitorId_cum, revenue_cum соответственно.
ordersAggregated[['transactionId_cum', 'visitorId_cum', 'revenue_cum']] = pd.concat(
[ordersAggregated[ordersAggregated['group'] == 'A'][['transactionId', 'visitorId', 'revenue']].cumsum(), \
ordersAggregated[ordersAggregated['group'] == 'B'][['transactionId', 'visitorId', 'revenue']].cumsum()])
ordersAggregated.head(10)
Аналогично с количеством пользователей.
visitorsAggregated = visitors.groupby(['date','group']).agg({'visitors':'sum'}).reset_index()
visitorsAggregated['visitors_cum'] = pd.concat(
[visitorsAggregated[visitorsAggregated['group'] == 'A']['visitors'].cumsum(), \
visitorsAggregated[visitorsAggregated['group'] == 'B']['visitors'].cumsum()])
visitorsAggregated.head(10)
Объединим обе таблицы в одной с понятными названиями столбцов. Лишние столбцы удалим.
cumulativeData = ordersAggregated.merge(visitorsAggregated, left_on=['date', 'group'], right_on=['date', 'group'])
del cumulativeData['transactionId']
del cumulativeData['visitorId']
del cumulativeData['revenue']
del cumulativeData['visitors']
cumulativeData.columns = ['date', 'group', 'orders', 'buyers', 'revenue', 'visitors']
cumulativeData.head(10)
Построим графики кумулятивной выручки по дням и группам A/B-тестирования.
# датафрейм с кумулятивным количеством заказов и кумулятивной выручкой по дням в группе А
cumulativeRevenueA = cumulativeData[cumulativeData['group']=='A'][['date','revenue', 'orders']]
# датафрейм с кумулятивным количеством заказов и кумулятивной выручкой по дням в группе B
cumulativeRevenueB = cumulativeData[cumulativeData['group']=='B'][['date','revenue', 'orders']]
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График кумулятивной выручки по дням и группам A/B-тестирования')
# строим график выручки группы А
plt.plot(cumulativeRevenueA['date'], cumulativeRevenueA['revenue'], label='A')
# строим график выручки группы B
plt.plot(cumulativeRevenueB['date'], cumulativeRevenueB['revenue'], label='B')
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Выручка заказов')
plt.legend();
График кумулятивной выручки группы B резко растёт 19-го августа, что свидетельствует о всплеске числа заказов, либо о появлении очень дорогих заказов в выборке.
Построим графики среднего чека по группам — разделим кумулятивную выручку на кумулятивное число заказов.
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График среднего чека по дням и группам A/B-тестирования')
plt.plot(cumulativeRevenueA['date'], cumulativeRevenueA['revenue']/cumulativeRevenueA['orders'], label='A')
plt.plot(cumulativeRevenueB['date'], cumulativeRevenueB['revenue']/cumulativeRevenueB['orders'], label='B')
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Средний чек заказов')
plt.legend();
Средний чек заказов группы A вначале проседает, потом быстро растёт со всплеском 13 августа, затем стабилизируется. Средний чек группы B растёт скачкообразно (всплеск 19-го августа на месте), затем медленно падает.
Построим график относительного изменения кумулятивного среднего чека. Добавим горизонтальную ось методом axhline() (horizontal line across the axis — «горизонтальная линия поперек оси»).
# собираем данные в одном датафрейме
mergedCumulativeRevenue = cumulativeRevenueA.merge(
cumulativeRevenueB, left_on='date', right_on='date', how='left', suffixes=['A', 'B'])
# cтроим отношение средних чеков
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График относительного изменения кумулятивного среднего чека группы B к группе A')
plt.plot(mergedCumulativeRevenue['date'], \
(mergedCumulativeRevenue['revenueB']/mergedCumulativeRevenue['ordersB'])/ \
(mergedCumulativeRevenue['revenueA']/mergedCumulativeRevenue['ordersA'])-1)
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Относительное изменение кумулятивного среднего чека')
# добавляем ось X
plt.axhline(y=0, color='black', linestyle='--', linewidth = 3);
Резкие различия в кумулятивном среднем чеке наблюдаются 4-го, 6-го и 8-го, 19-го августа.
Построим график кумулятивной конверсии по группам.
# считаем кумулятивную конверсию
cumulativeData['conversion'] = cumulativeData['orders']/cumulativeData['visitors']
# отделяем данные по группе A
cumulativeDataA = cumulativeData[cumulativeData['group']=='A']
# отделяем данные по группе B
cumulativeDataB = cumulativeData[cumulativeData['group']=='B']
# строим графики
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График кумулятивной конверсии по группам')
plt.plot(cumulativeDataA['date'], cumulativeDataA['conversion'], label='A')
plt.plot(cumulativeDataB['date'], cumulativeDataB['conversion'], label='B')
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Кумулятивная конверсия')
plt.legend();
Графики кумулятивной конверсии неплавные, есть выбросы. Кумулятивная конверсия группы B на 10-15% выше конверсии группы A.
Построим график относительного различия кумулятивных конверсий.
mergedCumulativeConversions = cumulativeDataA[['date','conversion']].merge(
cumulativeDataB[['date','conversion']], left_on='date', right_on='date', how='left', suffixes=['A', 'B'])
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График относительного различия кумулятивных конверсий')
plt.plot(mergedCumulativeConversions['date'], \
mergedCumulativeConversions['conversionB']/mergedCumulativeConversions['conversionA']-1, \
label="Относительный прирост конверсии группы B относительно группы A")
plt.axhline(y=0, color='black', linestyle='--', linewidth = 3)
plt.axhline(y=0.125, color='grey', linestyle='--', linewidth = 3)
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Относительное различие кумулятивных конверсий')
plt.legend();
В начале теста группа B просела относительно группы A, затем резко выросла и стабилизировалась.
Подсчитаем количество заказов по пользователям и посмотрим на результат.
ordersByUsers = orders.groupby('visitorId', as_index=False).agg({'transactionId' : pd.Series.nunique})
ordersByUsers.columns = ['visitorId','orders']
ordersByUsers.head(10)
Проанализируем количество заказов по пользователям методом describe().
ordersByUsers['orders'].describe()
Построим гистрограмму распределения количества заказов на одного пользователя.
ordersByUsers['orders'].hist(figsize=(12, 7), bins=11)
ax = plt.gca()
ax.set_xlabel('Заказы, в шт.')
ax.set_ylabel('Количество пользователей')
plt.title('Гистограмма распределения количества заказов на одного пользователя');
Большинство покупателей заказывали только один раз. Однако есть пользователи с 2-5 заказами. Построим точечную диаграмму числа заказов на одного пользователя.
x_values = pd.Series(range(0,len(ordersByUsers)))
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Точечная диаграмма числа заказов на одного пользователя')
ax = plt.gca()
ax.set_xlabel('Пользователи')
ax.set_ylabel('Количество заказов')
plt.scatter(x_values, ordersByUsers['orders']);
Есть пользователи с 2-5 заказами.
Определим 95 и 99 перцентили количества заказов на одного пользователя методом percentile() библиотеки Numpy.
np.percentile(ordersByUsers['orders'], [95, 99])
Не более 5% пользователей оформляли больше двух заказов. И 1% пользователей заказывал более четырёх раз. Примем 3 заказа на одного пользователя за нижнюю границу числа заказов.
Проанализируем распределение стоимостей заказов методом describe().
orders['revenue'].describe()
Построим гистограмму, ограничив верхнюю планку заказа 40 000.
orders['revenue'].hist(range=(1, 40000), figsize=(12, 7), bins=100)
ax = plt.gca()
ax.set_xlabel('Стоимость (до 40 000)')
ax.set_ylabel('Количество заказов')
plt.title('Гистограмма распределения стоимостей заказов');
Построим точечную диаграмму стоимостей заказов, ограничив её значение до 100 000.
x_values = pd.Series(range(0,len(orders['revenue'])))
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('Точечная диаграмма стоимостей заказов')
ax = plt.gca()
ax.set_xlabel('Пользователи')
ax.set_ylabel('Стоимость заказа (до 100 000)')
plt.ylim(0, 100000)
plt.scatter(x_values, orders['revenue']);
Разброс стоимостей заказов значительный, определить границу аномальных заказов затруднительно.
Определим 95 и 99 перцентили стоисмостей заказа.
np.percentile(orders['revenue'], [95, 99])
Не более 5% заказов дороже 28 000 рублей и не более 1% дороже 58 233. Примем 30 000 за нижнюю границу стоимости заказа.
Подготовим итоговую таблицу data.
visitorsADaily = visitors[visitors['group']=='A'][['date', 'visitors']]
visitorsADaily.columns = ['date', 'visitorsPerDateA']
visitorsACummulative = visitorsAggregated[visitorsAggregated['group'] == 'A'][['date', 'visitors_cum']]
visitorsACummulative.columns = ['date', 'visitorsCummulativeA']
# visitorsACummulative
visitorsBDaily = visitors[visitors['group']=='B'][['date', 'visitors']]
visitorsBDaily.columns = ['date', 'visitorsPerDateB']
visitorsBCummulative = visitorsAggregated[visitorsAggregated['group'] == 'B'][['date', 'visitors_cum']]
visitorsBCummulative.columns = ['date', 'visitorsCummulativeB']
# visitorsBCummulative
ordersADaily = orders[orders['group']=='A'][['date', 'transactionId', 'visitorId', 'revenue']]\
.groupby('date', as_index=False)\
.agg({'transactionId' : pd.Series.nunique, 'revenue' : 'sum'})
ordersADaily.columns = ['date', 'ordersPerDateA', 'revenuePerDateA']
ordersACummulative = ordersAggregated[ordersAggregated['group'] == 'A'][['date', 'transactionId_cum', 'revenue_cum']]
ordersACummulative.columns = ['date', 'ordersCummulativeA', 'revenueCummulativeA']
# ordersACummulative
ordersBDaily = orders[orders['group']=='B'][['date', 'transactionId', 'visitorId', 'revenue']]\
.groupby('date', as_index=False)\
.agg({'transactionId' : pd.Series.nunique, 'revenue' : 'sum'})
ordersBDaily.columns = ['date', 'ordersPerDateB', 'revenuePerDateB']
ordersBCummulative = ordersAggregated[ordersAggregated['group'] == 'B'][['date', 'transactionId_cum', 'revenue_cum']]
ordersBCummulative.columns = ['date', 'ordersCummulativeB', 'revenueCummulativeB']
# ordersBCummulative
data = ordersADaily.merge(ordersBDaily, left_on='date', right_on='date', how='left')\
.merge(ordersACummulative, left_on='date', right_on='date', how='left')\
.merge(ordersBCummulative, left_on='date', right_on='date', how='left')\
.merge(visitorsADaily, left_on='date', right_on='date', how='left')\
.merge(visitorsBDaily, left_on='date', right_on='date', how='left')\
.merge(visitorsACummulative, left_on='date', right_on='date', how='left')\
.merge(visitorsBCummulative, left_on='date', right_on='date', how='left')
data.head()
Названия столбцов в таблице:
date — дата;ordersPerDateA — количество заказов в выбранную дату в группе A;revenuePerDateA — суммарная выручка в выбранную дату в группе A;ordersPerDateB — количество заказов в выбранную дату в группе B;revenuePerDateB — суммарная выручка в выбранную дату в группе B;ordersCummulativeA — суммарное число заказов до выбранной даты включительно в группе A;revenueCummulativeA — суммарная выручка до выбранной даты включительно в группе A;ordersCummulativeB — суммарное количество заказов до выбранной даты включительно в группе B;revenueCummulativeB — суммарная выручка до выбранной даты включительно в группе B;visitorsPerDateA — количество пользователей в выбранную дату в группе A;visitorsPerDateB — количество пользователей в выбранную дату в группе B;visitorsCummulativeA — количество пользователей до выбранной даты включительно в группе A;visitorsCummulativeB — количество пользователей до выбранной даты включительно в группе B.Посчитаем статистическую значимость различия в конверсии между группами. Создадим переменные ordersByUsersA и ordersByUsersB со столбцами ['userId', 'orders']. В них для пользователей, которые заказывали хотя бы 1 раз, укажем число совершённых заказов.
ordersByUsersA = orders[orders['group']=='A'].groupby('visitorId', as_index=False).agg({'transactionId' : pd.Series.nunique})
ordersByUsersA.columns = ['visitorId', 'orders']
ordersByUsersB = orders[orders['group']=='B'].groupby('visitorId', as_index=False).agg({'transactionId' : pd.Series.nunique})
ordersByUsersB.columns = ['visitorId', 'orders']
ordersByUsersA
Для подготовки выборок к проверке критерием Манна-Уитни объявим переменные sampleA и sampleB, в которых пользователям из разных групп будет соответствовать количество заказов. Тем, кто ничего не заказал, будут соответствовать нули.
Переменная sampleA состоит из двух частей:
ordersByUsersA['orders'].data['visitorsPerDateA'].sum() - len(ordersByUsersA['orders']). Создадим объект pd.Series нужной длины:
pd.Series(0, index=np.arange(data['visitorsPerDateA'].sum() - len(ordersByUsersA['orders'])), name='orders')Создадим список индексов функцией np.arange(), создающей массив индексов в формате np.array, который требуется для pd.Series. Объединим последовательности функцией pd.concat() с добавлением параметра axis=0 — по строкам.
sampleA = pd.concat(
[ordersByUsersA['orders'],pd.Series(0, index=np.arange(data['visitorsPerDateA'].sum() - len(ordersByUsersA['orders'])), \
name='orders')], axis=0)
sampleB = pd.concat(
[ordersByUsersB['orders'],pd.Series(0, index=np.arange(data['visitorsPerDateB'].sum() - len(ordersByUsersB['orders'])), \
name='orders')], axis=0)
sampleA
Сформулируем следующие гипотезы.
Нулевая гипотеза: конверсии групп A и B по «сырым» данным совпадают.
Альтернативная гипотеза: конверсии групп A и B по «сырым» данным различаются.
Применим критерий и отформатируем p-value, округлив его до трёх знаков после запятой. Выведем относительный прирост конверсии группы B: конверсия группы B / конверсия группы A - 1. Округлим до трёх знаков после запятой.
alpha = .05 # критический уровень статистической значимости
results = st.mannwhitneyu(sampleA, sampleB)
print('p-значение: ', "{0:.3f}".format(results.pvalue))
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу: разница статистически значима")
else:
print("Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя")
print("{0:.3f}".format(sampleB.mean()/sampleA.mean()-1))
По «сырым» данным в конверсии между группами A и B есть статистически значимое различие. Конверсия группы B на 14% выше.
Сформулируем следующие гипотезы.
Нулевая гипотеза: средний чек заказа групп A и B по «сырым» данным совпадает.
Альтернативная гипотеза: средний чек заказа групп A и B по «сырым» данным различается.
Рассчитаем статистическую значимость различий в среднем чеке между сегментами, передадав критерию mannwhitneyu() данные о выручке с заказов. Найдём относительные различия в среднем чеке между группами.
results = st.mannwhitneyu(orders[orders['group']=='A']['revenue'], orders[orders['group']=='B']['revenue'])
print('p-значение: ', "{0:.3f}".format(results.pvalue))
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу: разница статистически значима")
else:
print("Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя")
print("{0:.3f}".format(orders[orders['group']=='B']['revenue'].mean()/orders[orders['group']=='A']['revenue'].mean()-1))
По «сырым» данным различий в среднем чеке заказа между группами A и B нет. Средний чек группы B на четверть выше среднего чека группы A.
Сделаем срезы пользователей с числом заказов больше 3 — usersWithManyOrders и пользователей, совершивших заказы дороже 30 000 — usersWithExpensiveOrders. Объединим их в таблице abnormalUsers. Выведем их количество.
usersWithManyOrders = pd.concat(
[ordersByUsersA[ordersByUsersA['orders'] > 3]['visitorId'], \
ordersByUsersB[ordersByUsersB['orders'] > 3]['visitorId']], axis = 0)
usersWithExpensiveOrders = orders[orders['revenue'] > 30000]['visitorId']
abnormalUsers = pd.concat([usersWithManyOrders, usersWithExpensiveOrders], axis = 0).drop_duplicates().sort_values()
abnormalUsers.shape
Всего 57 аномальных пользователей.
Узнаем, как их действия повлияли на результаты теста. Посчитаем статистическую значимость различий в конверсии между группами теста по «очищенным» данным. Сначала подготовим выборки количества заказов по пользователям по группам теста.
sampleAFiltered = pd.concat(
[ordersByUsersA[np.logical_not(ordersByUsersA['visitorId'].isin(abnormalUsers))]['orders'], \
pd.Series(0, index=np.arange(data['visitorsPerDateA'].sum() - len(ordersByUsersA['orders'])),name='orders')],axis=0)
sampleBFiltered = pd.concat(
[ordersByUsersB[np.logical_not(ordersByUsersB['visitorId'].isin(abnormalUsers))]['orders'], \
pd.Series(0, index=np.arange(data['visitorsPerDateB'].sum() - len(ordersByUsersB['orders'])),name='orders')],axis=0)
Сформулируем следующие гипотезы.
Нулевая гипотеза: конверсии групп A и B по «очищенным» данным совпадают.
Альтернативная гипотеза: конверсии групп A и B по «очищенным» данным различаются.
Применим статистический критерий Манна-Уитни к полученным выборкам.
results = st.mannwhitneyu(sampleAFiltered, sampleBFiltered)
print('p-значение: ', "{0:.3f}".format(results.pvalue))
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу: разница статистически значима")
else:
print("Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя")
print("{0:.3f}".format(sampleBFiltered.mean()/sampleAFiltered.mean()-1))
Результаты по конверсии практически не изменились. Группа B по-прежнему лучше группы A.
Сформулируем следующие гипотезы.
Нулевая гипотеза: средний чек заказа групп A и B по «очищенным» данным совпадает.
Альтернативная гипотеза: средний чек заказа групп A и B по «очищенным» данным различается.
Посчитаем статистическую значимость различий в среднем чеке заказа между группами по очищенным данным.
results = st.mannwhitneyu(
orders[np.logical_and(
orders['group']=='A',
np.logical_not(orders['visitorId'].isin(abnormalUsers)))]['revenue'],
orders[np.logical_and(
orders['group']=='B',
np.logical_not(orders['visitorId'].isin(abnormalUsers)))]['revenue'])
print('p-значение: ', "{0:.3f}".format(results.pvalue))
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу: разница статистически значима")
else:
print("Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя")
print("{0:.3f}".format(
orders[np.logical_and(orders['group']=='B',np.logical_not(orders['visitorId'].isin(abnormalUsers)))]['revenue'].mean()/
orders[np.logical_and(
orders['group']=='A',
np.logical_not(orders['visitorId'].isin(abnormalUsers)))]['revenue'].mean() - 1))
P-value увеличился, разница в среднем чеке стала почти нулевой.
Сравним кумулятивный среднего чека по группам по «очищенным» данным. Сохраним кумулятивные данные о заказах в таблицах ordersAFiltered и ordersBFiltered.
ordersAFiltered = orders[np.logical_and(
orders['group']=='A',
np.logical_not(orders['visitorId'].isin(abnormalUsers)))][['date', 'transactionId', 'revenue']].groupby(
['date']).agg({'transactionId':'nunique', 'revenue':'sum'}).cumsum().reset_index()
ordersBFiltered = orders[np.logical_and(
orders['group']=='B',
np.logical_not(orders['visitorId'].isin(abnormalUsers)))][['date', 'transactionId', 'revenue']].groupby(
['date']).agg({'transactionId':'nunique', 'revenue':'sum'}).cumsum().reset_index()
ordersAFiltered.head()
Построим график.
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График среднего чека по очищенным данным по дням и группам A/B-тестирования')
plt.plot(ordersAFiltered['date'], ordersAFiltered['revenue'] / ordersAFiltered['transactionId'], label='A')
plt.plot(ordersBFiltered['date'], ordersBFiltered['revenue'] / ordersBFiltered['transactionId'], label='B')
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Средний чек заказов')
plt.legend();
Построим график относительного изменения кумулятивного среднего чека по «очищенным» данным.
# собираем данные в одном датафрейме
mergedCumulativeRevenueFiltered = ordersAFiltered.merge(
ordersBFiltered, left_on='date', right_on='date', how='left', suffixes=['A', 'B'])
mergedCumulativeRevenueFiltered.tail()
# cтроим отношение средних чеков
plt.figure(figsize=(12, 7))
plt.grid(True)
plt.title('График относительного изменения кумулятивного среднего чека группы B к группе A по очищенным данным')
plt.plot(mergedCumulativeRevenueFiltered['date'], \
(mergedCumulativeRevenueFiltered['revenueB']/mergedCumulativeRevenueFiltered['transactionIdB'])/ \
(mergedCumulativeRevenueFiltered['revenueA']/mergedCumulativeRevenueFiltered['transactionIdA'])-1)
ax = plt.gca()
ax.minorticks_on()
ax.grid(which='major', color = 'k')
ax.grid(which='minor')
ax.set_xlabel('Дни заказов')
ax.set_ylabel('Относительное изменение кумулятивного среднего чека')
# добавляем ось X
plt.axhline(y=0, color='black', linestyle='--', linewidth = 3);
Есть статистически значимое различие по конверсии между группами как по «сырым» данным, так и по «очищенным». График различия конверсии между группами показывает, что результаты группы B лучше группы A.
Нет статистически значимого различия по среднему чеку между группами ни по «сырым» данным, ни по «очищенным». После удаления аномалий средний чек группы B стал незначительно ниже группы A.
Исходя из обнаруженных фактов, тест следует остановить и признать его неуспешным. Несмотря на рост конверсии, средний чек незначительно снизился.
Есть статистически значимое различие по конверсии между группами как по «сырым» данным, так и по «очищенным». График различия конверсии между группами показывает, что результаты группы B лучше группы A.
Нет статистически значимого различия по среднему чеку между группами ни по «сырым» данным, ни по «очищенным». После удаления аномалий средний чек группы B стал незначительно ниже группы A.
График кумулятивной выручки группы B резко растёт 19-го августа, что свидетельствует о всплеске числа заказов, либо о появлении очень дорогих заказов в выборке.
Средний чек заказов группы A вначале проседает, затем быстро растёт со всплеском 13 августа, затем стабилизируется. Средний чек группы B растёт скачкообразно (всплеск 19-го августа на месте), затем медленно падает.
Резкие различия в кумулятивном среднем чеке наблюдаются 4-го, 6-го и 8-го, 19-го августа.
Графики кумулятивной конверсии неплавные, есть выбросы. Кумулятивная конверсия группы B на 10-15% выше конверсии группы A.
В начале теста группа B просела относительно группы A, затем резко выросла и стабилизировалась.
Большинство покупателей заказывали только один раз, но есть пользователи с 2-5 заказами и выше. Не более 5% пользователей оформляли больше двух заказов. И 1% пользователей заказывал более четырёх раз. Примем 3 заказа на одного пользователя за нижнюю границу числа заказов.
Не более 5% заказов дороже 28 000 рублей и не более 1% дороже 58 233. Примем 30 000 за нижнюю границу стоимости заказа.
По «сырым» данным в конверсии между группами A и B есть статистически значимое различие. Конверсия группы B на 14% выше конверсии группы A. По «очищенным» данным результаты по конверсии практически не изменились. Группа B по-прежнему лучше группы A.
По «сырым» данным различий в среднем чеке заказа между группами A и B нет. Средний чек группы B на четверть выше среднего чека группы A. По «очищенным» данным p-value увеличился, разница в среднем чеке стала почти нулевой.